
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;


using Swaf;
using Swaf.BizObj;
using Swaf.Logging;

namespace Swaf.DataAccess
{
	/// <summary>
	/// Summary description for AdoNetBizObjFormatter.
	/// </summary>
	public class AdoNetBizObjFormatter : IDataFormatter
	{
		protected enum FormatterBehavior {addNew, mustMatch, tryMatch};

		protected MatchNodeInfo m_baseNode;
		protected IBizObjSource m_results;
		protected AdoNetDataResponse m_resp;
		protected string m_bizObjSpec;
		protected Regex m_bizObjSpecElement;
		protected DataTable m_readerStructure;
		protected SourceType m_dataSourceType;
		protected string m_typeInfo;
		protected FormatterBehavior m_behavior = FormatterBehavior.addNew;
		protected string m_mergeListName;

		protected IApplication m_app;

		public AdoNetBizObjFormatter(Match mr, object[] extraInfo)
		{
			if(extraInfo != null && extraInfo.Length > 0 && extraInfo[0] is AdoNetDataResponse)
			{
				m_resp = (AdoNetDataResponse)extraInfo[0];
				m_app = Application.currentApp;

				m_bizObjSpec = mr.Groups[2].Value;
				m_bizObjSpecElement = Application.currentApp.resMgr.knownGoodPattern(
					"{starts-with}{optional-ws}{optional ,}{optional-ws}({identifier}){optional-ws}{optional-balanced()}{optional-ws}");

				if(mr.Groups.Count == 4)
				{
					if(String.Compare(mr.Groups[1].Value, "merge") == 0)
						m_behavior = FormatterBehavior.tryMatch;
					else if(String.Compare(mr.Groups[1].Value, "match merge") == 0)
						m_behavior = FormatterBehavior.mustMatch;
					m_mergeListName = mr.Groups[3].Value.Trim();
				}
				else
				{
					//As of june 8,2003 The BasicSource far outperforms XmlSource for this class
					//by almost 1.5 orders of magnatude.  There must be some performance flaw in either
					//the XmlSource implementation or the .NET XML library.
					//TODO: Read this info from config
					//m_dataSourceType = SourceType.XmlSource;
					//m_typeInfo = "<BizObjTree />";
					m_dataSourceType = SourceType.BasicSource;
					m_typeInfo = "";
				}
			}
		}

		internal string MergeListName { get { return m_mergeListName; } }
		#region IDataFormatter Members

		public object format()
		{
			object o = null;
			try 
			{
				IDataReader reader = m_resp.Reader;
				if(reader != null)
				{
					m_readerStructure = reader.GetSchemaTable();
					if (m_readerStructure == null)
					{
						throw new DataFormatException("No results set data schema was specified, can not format results");
					}

					m_baseNode = buildMatchNodeInfo(m_bizObjSpec, null);
					if(m_mergeListName != null)
					{
						o = m_resp.getPassedItem(m_mergeListName);
					}
					else
					{
						//m_results = Application.currentApp.bizObjMgr.createSource(m_dataSourceType, m_typeInfo);
						//o = m_results.getList(m_baseNode.Info.Name,  null);
						o = Swaf.BizObj.basic.FullBizObjList.createInstance("", Application.currentApp.bizObjMgr.getDefinition(m_baseNode.Info.Name));
					}
					m_baseNode.ParentList = (IBizObjList)o;
					int recordsProcessed = 0;
					while(reader.Read())
					{
						processNode(m_baseNode, reader);
						++recordsProcessed;
					}
					m_baseNode.ParentList.refresh();
				}
			}
			catch (DataFormatException)
			{
				// Specify this here so that we create too much of an exception tree
				throw;
			}
			catch(Exception e)
			{
				throw new DataFormatException(this.GetType().Name, m_bizObjSpec, e);
			}
			
			return o;
		}

		#endregion

		protected MatchNodeInfo buildMatchNodeInfo(string bizObjSpec, MatchNodeInfo parent)
		{
			MatchNodeInfo matchNode = null;
			string name = "";
			Match mr = m_bizObjSpecElement.Match(bizObjSpec);

			//Step 1: process bizObjSpec to build MatchNodeInfo heirachy with only
			//the BizObjInfo being set for each node.

			if(mr.Success)
			{
				name = mr.Groups[1].Value;
				string children = mr.Groups[2].Value;

				bool isList = true;
				IBizObjDefinition info = null;
				if(parent != null)
				{
					IBizObjField finfo = parent.Info.getField(name, true);
					if(finfo != null)
					{
						if(finfo.BizType == BizFieldType.BizObjList)
							info = ((IBizObjList)finfo.SampleValue).objectType;
						else if(finfo.BizType == BizFieldType.BizObj)
						{
							info = ((IBizObj)finfo.SampleValue).type;
							isList = false;
						}
					}
				}
				else
					info = m_app.bizObjMgr.getDefinition(name);
			
				if(info == null)
					throw new BadBizObjTypeException(name);

				matchNode = new MatchNodeInfo(name, info, isList);
				while(children != null && children.Length > 0)
				{
					if((mr = m_app.resMgr.matches(m_bizObjSpecElement, children))!= null) 
					{
						if(mr.Groups[2].Value == null)
							matchNode.Children[mr.Groups[1].Value]= buildMatchNodeInfo(mr.Groups[1].Value, matchNode);
						else
							matchNode.Children[mr.Groups[1].Value]= buildMatchNodeInfo(mr.Groups[1].Value + "(" + mr.Groups[2].Value + ")", matchNode);
						children = children.Substring(mr.Length);
					}	
				}
			
				//Step 2: loop through each field in the ResultSet and find a matching
				//BizObjInfo.field.name in this MatchNodeInfo object.
				string upperName = name.ToUpper();
				try 
				{
					IDictionary aliasDecoFields = null;

					int numCols = m_readerStructure.Columns.Count;
					foreach(DataRow col in m_readerStructure.Rows)
					{
                        IBizObjField bizObjField = null;
                        string colName = col["ColumnName"].ToString().ToUpper();
						string realColName = (colName.StartsWith(upperName)) ? colName.Substring(name.Length) : colName;

                        string mappedName = (string)m_resp.RequestInfo.FieldMappings[colName];
                        if (mappedName != null && (bizObjField = info.getField(mappedName, false)) == null && mappedName.StartsWith(upperName))
                            bizObjField = info.getField(mappedName.Substring(name.Length), false);
      

                        //if bizObjField is still null then either there was no mapped name specified in the DAR or
                        //the mapped name is for some other bizobj in the hierarchy.  If there was a mappedName for the
                        //column in the data reader but it wasn't for this bizobj, then dont bother trying to map it
                        //up other ways with the code below.  When a developer applies a mapping they are stating they 
                        //know that the field is for a very specific element in the bizobj tree of the formatter.
                        if (bizObjField == null && mappedName == null)
                        {
                            if ((bizObjField = info.getField(colName, false)) != null)
                            {
                                // Column name maps directly to the BizObjField name
                                realColName = colName;
                            }
                            else if (realColName != colName && ((bizObjField = info.getField(realColName, false)) != null))
                            {
                                // Column name was prefixed with the BizObj Type Name

                                // This line of code technically unnessary but kept for clarity
                                realColName = realColName;
                            }
                            else
                            {
                                // Didn't find BizObjField so check to see if the field has an dbAlias decorator 
                                // and find it that way

                                // Build the map of alias Fields for this bizobj defintion
                                // This will only be done at MOST once per during this foreach loop
                                if (aliasDecoFields == null)
                                {
                                    aliasDecoFields = new Hashtable();
                                    foreach (IBizObjField tmpField in info.Fields.Values)
                                    {
                                        IDecorator aliasDecoField = tmpField.Decorators["dbAlias"] as IDecorator;
                                        if (aliasDecoField != null)
                                        {
                                            // Add element to the decorated fields
                                            aliasDecoFields[aliasDecoField.Value.ToUpper()] = tmpField;
                                        }
                                    }
                                }

                                // Try finding the field via the alias decorator, first try colName, then try
                                // colName stripped of the bizobj type name
                                if ((bizObjField = aliasDecoFields[colName] as IBizObjField) == null)
                                {
                                    // Wasn't found with colName, now try colName stripped of the bizobj type  name
                                    bizObjField = aliasDecoFields[realColName] as IBizObjField;
                                }
                            }
                        }

						if(bizObjField != null)
						{
                            realColName = bizObjField.Name.ToUpper();
							bool convertType = false;
							bool hasDefaultField = false;

							if ( (bizObjField.BizType == BizFieldType.BizObj) && ((IBizObj) bizObjField.SampleValue).type.Fields.Contains(""))
								hasDefaultField = true;

							//This could be expanded more to check for bizobj field types that are
							//different from the types that will come back from the IDataReader.
							//Guid types from the IDataReader need to be inserted as BGuid.
							if ( !hasDefaultField && (bizObjField.FactoryType == "Guid" || bizObjField.FactoryType == "Binary" ||
								bizObjField.BizType == BizFieldType.BizObj || bizObjField.BizType == BizFieldType.BizObjList))
								convertType = true;

							matchNode.Fields[realColName] = 
								new BizObjField(bizObjField, (int)col["ColumnOrdinal"], convertType, hasDefaultField);
						}
	
					}
				}
				catch(Exception e) 
				{
					throw new Swaf.DataAccess.DataFormatException(GetType().Name, bizObjSpec, e);
				}
			}
			return matchNode;
		}
	
		protected void processNode(MatchNodeInfo node, IDataReader reader) 
		{
			IComparable currentRsNodeKey = null;
			bool createdNewNode = false;
			IBizObj obj = null;
			try
			{
				if(node.IsList)
				{
					//Step 1: if node.lastBizObjKey() != makeNodeKey(node.info(), recordset)
					currentRsNodeKey = makeNodeKey(node, reader);
					//m_app.Trace.write(1, "\n++processNode {0} currentRsNodeKey: {1}", node.Name, currentRsNodeKey);
					if(currentRsNodeKey == null)
					{
						return;
					}
				}
			}
			catch (DataFormatException)
			{
				// Handle this so it doesn't fall into the generic exception handler because this is something that
				// this class has thrown purposefully
				throw;
			}
			catch(Exception e)
			{
				throw new Swaf.DataAccess.DataFormatException(string.Format("No key node exists so object can not be processed for format {0} in node {1}.", this.m_bizObjSpec, node.Name), e);
			}

			try
			{
				if(!node.IsList || node.LastKey == null || !node.LastKey.Equals(currentRsNodeKey))
				{
					node.LastKey = currentRsNodeKey;
					//Step 0: Because this node has had its key changed, all of its children need to have their key
					//        reset to null.
					foreach(MatchNodeInfo childNode in node.Children.Values)
					{
						childNode.LastKey = null;

						// NOTE: node.LastKey will be null if the parent of this element is a BizObj and not a BizObjList
						// that is why we are checking node.LastKey != null, we don't want to reset the position in this case
						if (node.LastKey != null && childNode.IsList)
						{
							childNode.ParentListPos = 0;
						}
					}

					//Step 1.A: Make new bizobj via node.parentList().createNew()
					if(node.IsList)
					{
						if(m_behavior == FormatterBehavior.addNew)
						{
							obj = node.ParentList.insertNew();
						}
						else
						{
							//Try and find a match
							IBizObj o = node.ParentList.numElements <= node.ParentListPos?null:node.ParentList[node.ParentListPos];
							if(o == null)
							{
								obj = node.ParentList.insertNew();
								++node.ParentListPos;
							}
							else
							{
								IComparable  listElementKey = (IComparable)o.KeyValue;
                                if (listElementKey is CompositeComparable)
                                    ((CompositeComparable)listElementKey).HasConflictingEncoding = (m_resp.RequestInfo.DbConnection.encoding == BasicConnectionItem.ASCIIEncoding);

                                int p = 0;
                                p = encodingCompareTo(currentRsNodeKey, listElementKey);
								if(p == 0)
								{
									//									m_app.Trace.write(1, "processNode {0} match found {1}", node.Name, currentRsNodeKey);
									obj = o;
									++node.ParentListPos;
								}
								else if(p < 0)
								{
									//									m_app.Trace.write(1, "processNode {0} parentList element less than key, advancing in list {1}", node.Name, currentRsNodeKey);
									bool keyFound = false;
									while(p < 0 && ++node.ParentListPos < node.ParentList.numElements)
									{
										o = node.ParentList[node.ParentListPos];
										listElementKey = (IComparable)o.KeyValue;
                                        if (listElementKey is CompositeComparable)
                                            ((CompositeComparable)listElementKey).HasConflictingEncoding = (m_resp.RequestInfo.DbConnection.encoding == BasicConnectionItem.ASCIIEncoding);
                                        if (encodingCompareTo(currentRsNodeKey, listElementKey) == 0)
										{
											obj = o;
											++node.ParentListPos;
											keyFound = true;
											break;
										}
									}

									if (! keyFound)
									{
										throw new DataFormatException(string.Format("Attempting to locate key {0} for format {1} in node {2}.  Check resultset sort order and for parentless children.", currentRsNodeKey, m_bizObjSpec, node.Name));
									}
								}
								else // p > 0
								{
									// CUSP00039568 - Don't allow parentless children
									throw new DataFormatException(string.Format("Attempting to locate key {0} for format {1} in node {2}.  Check resultset sort order and for parentless children.", currentRsNodeKey, m_bizObjSpec, node.Name));
								}
							}
						}
					}
					else
						obj = node.ParentObj;

					//Step 1.B: foreach field in node.fields, bizobj.put(field.name, rs.getObject(field.index));
					try 
					{
						object val = null;
						BizObjField ftemp = null;
						foreach(BizObjField finfo in node.Fields.Values)
						{
							ftemp = finfo;
							val = reader.GetValue(finfo.ReaderIndex);
							if(finfo.ConvertType && val != null && val != DBNull.Value)
							{
								if((finfo.FieldInfo.BizType == BizFieldType.BizObj ||
									finfo.FieldInfo.BizType == BizFieldType.BizObjList) && (!finfo.HasDefaultField))
								{
									//Console.WriteLine("{0}:Starting to deserialize", System.Threading.Thread.CurrentThread.Name);
									BinaryFormatter formatter = new BinaryFormatter();
									Binary bin = new Binary((byte[])val);
									//MemoryStream s = new MemoryStream((byte[])val);
									DateTime t1 = DateTime.Now;
									val = formatter.Deserialize(bin.DataStream);
									bin.Dispose();
									//s.Close();
									DateTime t2 = DateTime.Now;
									Console.WriteLine("{1}:Time to deserialize {0}", t2 -t1, System.Threading.Thread.CurrentThread.Name);
								}
								else
									val = BaseBizObj.BizObjFieldFactory.createFromNickName(finfo.FieldInfo.FactoryType, val);
							}
							if(val != null && val != System.DBNull.Value && obj != null)
								if(finfo.ExtraInfo == null)
									if (finfo.HasDefaultField)
									{
										IBizObj cont = (IBizObj) finfo.FieldInfo.SampleValue;
										cont.put("",val);
										obj.put(finfo.FieldName, cont);
									}
									else
									{
										obj.put(finfo.FieldName, val);
									}
								else
									obj.put(finfo.FieldName, val, finfo.ExtraInfo);
						}
					}
					catch(Exception e) 
					{
						throw new Swaf.DataAccess.DataFormatException(string.Format("Unable to handle field information formatting bizObj {0} using format {1}.", (obj != null) ? obj.type.Name : "unknown", m_bizObjSpec), e);
					}

					if(obj != null)
						createdNewNode = true;
				}
				else
				{
					//					if (node.IsList)
					//					{
					//						m_app.Trace.write(1, "processNode {0) key same as last element {1}", node.Name, currentRsNodeKey);
					//					}
				}

				try
				{
					//Step 2: foreach childBizObj in node.children(), processNode(childBizObj)
					foreach(MatchNodeInfo childNode in node.Children.Values)
					{
						if(createdNewNode)
						{
							if(childNode.IsList)
							{
								// Set the child node's parent list to the new node that was just created
								childNode.ParentList = (IBizObjList)obj.get(childNode.Name);
							}
							else
							{
								// Set the child node's parent to the new node that was created
								childNode.ParentObj = (IBizObj)obj.get(childNode.Name);
							}
						}

						try
						{
							// process the child node
							processNode(childNode, reader);
						}
						catch(Exception ee)
						{
							//Application.currentApp.Trace.write(1,ee.ToString());
							throw new Swaf.DataAccess.DataFormatException(string.Format("Failed to recusively call process node for childnode {0} using format {1}.", childNode.Name, m_bizObjSpec), ee);
						}

					}
					if (obj != null)
					{
						obj.clearDirtyFlags();
						obj.isNew=false;
					}
				}
				catch (Swaf.DataAccess.DataFormatException)
				{
					// threw the exception ourselves don't add to the exception stack
					throw;
				}
				catch(Exception e)
				{
					throw new Swaf.DataAccess.DataFormatException(string.Format("Failure handling child biz obj for parent {0}, format {1}", (obj != null) ? obj.type.Name : "unknown", m_bizObjSpec), e);
				}
			}
			catch (Swaf.DataAccess.DataFormatException)
			{
				// threw the exception ourselves don't add to the exception stack
				throw;
			}
			catch(Exception e)
			{
				throw new Swaf.DataAccess.DataFormatException(string.Format("Failure formatting node {0}, format {1}", node.Name, m_bizObjSpec), e);
			}
		}

        protected int encodingCompareTo(IComparable currentRsNodeKey, IComparable listElementKey)
        {
            int compareResults = 0;

            if (m_resp.RequestInfo.DbConnection.encoding == BasicConnectionItem.ASCIIEncoding &&
                (listElementKey is string) && currentRsNodeKey is string)
            {
                // Use a culture compare CompareOptions.StringSort parameters if the encoding doesn't
                // match the encoding of the system
                compareResults = CultureInfo.CurrentCulture.CompareInfo.Compare(listElementKey as string, currentRsNodeKey as string, CompareOptions.StringSort);
            }
            else
            {
                compareResults = listElementKey.CompareTo(currentRsNodeKey);
				//compareResults = currentRsNodeKey.CompareTo(listElementKey);
            }
            return compareResults;
        }
	
		protected IComparable makeNodeKey(MatchNodeInfo node, IDataReader reader)
		{
			IList fields = node.Info.KeyFields;
			bool keysAllNull=true;
			if(fields == null)
			{
				throw new DataAccessException(String.Format("Cannot use bizobj types that do not define a key: BizObj type '{0}' doesn't have a key defined",node.Info.Name));
			}

			BizObjField i = null;
			object x = 'x';
			if(fields.Count == 1)
			{
				try
				{
					i = (BizObjField)node.Fields[((IBizObjField)fields[0]).Name.ToUpper()];
					x = reader.GetValue(i.ReaderIndex);
					if(x is System.DBNull)
						return null;
					if (x is System.Guid)
						x = new Swaf.BGuid.Guid((System.Guid)x);
					return (IComparable)x;
				}
				catch(Exception e)
				{
					throw new Swaf.DataAccess.DataFormatException(
                        string.Format("BizObjFormatter unable to find key field {0} in the db resultset.  The query must return the key values for each bizobj in the formatter to have the results formatted correctly.", ((IBizObjField)fields[0]).Name), e);
				}
			}
			else
			{
				CompositeComparable compositeComparable = new CompositeComparable(m_resp.RequestInfo.DbConnection.encoding == BasicConnectionItem.ASCIIEncoding);
				keysAllNull = true;

				foreach(IBizObjField info in fields)
				{
					string keyName=info.Name.ToUpper();
					BizObjField fieldInfo =(BizObjField)node.Fields[keyName];
					if (fieldInfo != null)
					{
						int readerIndex = fieldInfo.ReaderIndex;
						object keyObj = reader.GetValue(readerIndex);
						if ( !(keyObj is System.DBNull) && (keyObj != null) )
						{
							keysAllNull=false;
						}

						compositeComparable.Add(((keyObj is System.DBNull) ? null : keyObj));
					}
					else
					{
						throw new Swaf.DataAccess.DataAccessException(string.Format("Unable to obtain field info for field {0} in BizObj {1}",keyName, node.Name));
					}
				}
				if (! keysAllNull)
					return compositeComparable;
				else
					return null;
			}
		}

		protected class MatchNodeInfo
		{
			public IDictionary Fields;
			public IDictionary Children;
			public object LastKey;
			public IBizObjDefinition Info;
			public IBizObjList ParentList;
			public IBizObj ParentObj;
			public string Name;
			public bool IsList;
			public int ParentListPos = 0;

			public MatchNodeInfo(string name, IBizObjDefinition info, bool isList) 
			{
				Name = name;
				Fields = new Hashtable();
				Children = new Hashtable();
				Info = info;
				IsList = isList;
			}
		}

		protected class BizObjField
		{
			protected int m_readerIndex;
			public IBizObjField FieldInfo;
			public bool ConvertType = false;
			public bool HasDefaultField = false;

			/// <summary>
			/// Used as the extraInfo parameter to IBizObj.put(...) when populating the bizobj
			/// with data from the resultset.  This allows decorated values stored in the database
			/// to be pushed into the bizobj properly.  As of Aug-04 the only bizobj decorator
			/// specifically supported is 'encrypt'.
			/// </summary>
			protected string m_extraInfo = null;

			public BizObjField(IBizObjField field, int readerIndex, bool convertType, bool hasDefaultField)
			{
				m_readerIndex = readerIndex;
				ConvertType = convertType;
				HasDefaultField = hasDefaultField;
				FieldInfo = field;
				m_extraInfo = "externalTrusted";
			}
			public string FieldName{get{return FieldInfo.Name;}}
			public int ReaderIndex{get{return m_readerIndex;}}
			public string ExtraInfo{get{return m_extraInfo;}}
		}
	}
}
